package org.kazuhiko.pokemon3d.utility.loader;

import static org.lwjgl.opengl.GL11.GL_AMBIENT;
import static org.lwjgl.opengl.GL11.GL_COMPILE;
import static org.lwjgl.opengl.GL11.GL_DIFFUSE;
import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_FRONT;
import static org.lwjgl.opengl.GL11.GL_SHININESS;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
import static org.lwjgl.opengl.GL11.glBegin;
import static org.lwjgl.opengl.GL11.glColor3f;
import static org.lwjgl.opengl.GL11.glEnd;
import static org.lwjgl.opengl.GL11.glEndList;
import static org.lwjgl.opengl.GL11.glGenLists;
import static org.lwjgl.opengl.GL11.glMaterial;
import static org.lwjgl.opengl.GL11.glMaterialf;
import static org.lwjgl.opengl.GL11.glNewList;
import static org.lwjgl.opengl.GL11.glNormal3f;
import static org.lwjgl.opengl.GL11.glNormalPointer;
import static org.lwjgl.opengl.GL11.glTexCoord2f;
import static org.lwjgl.opengl.GL11.glVertex3f;
import static org.lwjgl.opengl.GL11.glVertexPointer;
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
import static org.lwjgl.opengl.GL15.glBindBuffer;
import static org.lwjgl.opengl.GL15.glBufferData;
import static org.lwjgl.opengl.GL15.glGenBuffers;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.nio.FloatBuffer;

import org.kazuhiko.pokemon3d.utility.Model;
import org.kazuhiko.pokemon3d.utility.tools.BufferTools;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.vector.Vector2f;
import org.lwjgl.util.vector.Vector3f;
import org.newdawn.slick.opengl.TextureLoader;

public class OBJLoader
{

	public static int createDisplayList( Model m )
	{
		int displayList = glGenLists( 1 );
		glNewList( displayList, GL_COMPILE );
		{
			glMaterialf( GL_FRONT, GL_SHININESS, 120 );
			glColor3f( 0.4f, 0.27f, 0.17f );
			glBegin( GL_TRIANGLES );
			for ( Model.Face face : m.getFaces() )
			{
				if ( face.hasNormals() )
				{
					Vector3f n1 = m.getNormals().get( face.getNormalIndices()[ 0 ] - 1 );
					glNormal3f( n1.x, n1.y, n1.z );
				}
				Vector3f v1 = m.getVertices().get( face.getVertexIndices()[ 0 ] - 1 );
				glVertex3f( v1.x, v1.y, v1.z );
				if ( face.hasNormals() )
				{
					Vector3f n2 = m.getNormals().get( face.getNormalIndices()[ 1 ] - 1 );
					glNormal3f( n2.x, n2.y, n2.z );
				}
				Vector3f v2 = m.getVertices().get( face.getVertexIndices()[ 1 ] - 1 );
				glVertex3f( v2.x, v2.y, v2.z );
				if ( face.hasNormals() )
				{
					Vector3f n3 = m.getNormals().get( face.getNormalIndices()[ 2 ] - 1 );
					glNormal3f( n3.x, n3.y, n3.z );
				}
				Vector3f v3 = m.getVertices().get( face.getVertexIndices()[ 2 ] - 1 );
				glVertex3f( v3.x, v3.y, v3.z );
			}
			glEnd();
		}
		glEndList();
		return displayList;
	}

	private static FloatBuffer reserveData( int size )
	{
		return BufferUtils.createFloatBuffer( size );
	}

	private static float[] asFloats( Vector3f v )
	{
		return new float[]
		{ v.x, v.y, v.z };
	}

	public static int[] createVBO( Model model )
	{
		int vboVertexHandle = glGenBuffers();
		int vboNormalHandle = glGenBuffers();
		// TODO: Implement materials with VBOs
		FloatBuffer vertices = reserveData( model.getFaces().size() * 9 );
		FloatBuffer normals = reserveData( model.getFaces().size() * 9 );
		for ( Model.Face face : model.getFaces() )
		{
			vertices.put( asFloats( model.getVertices().get( face.getVertexIndices()[ 0 ] - 1 ) ) );
			vertices.put( asFloats( model.getVertices().get( face.getVertexIndices()[ 1 ] - 1 ) ) );
			vertices.put( asFloats( model.getVertices().get( face.getVertexIndices()[ 2 ] - 1 ) ) );
			normals.put( asFloats( model.getNormals().get( face.getNormalIndices()[ 0 ] - 1 ) ) );
			normals.put( asFloats( model.getNormals().get( face.getNormalIndices()[ 1 ] - 1 ) ) );
			normals.put( asFloats( model.getNormals().get( face.getNormalIndices()[ 2 ] - 1 ) ) );
		}
		vertices.flip();
		normals.flip();
		glBindBuffer( GL_ARRAY_BUFFER, vboVertexHandle );
		glBufferData( GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW );
		glVertexPointer( 3, GL_FLOAT, 0, 0L );
		glBindBuffer( GL_ARRAY_BUFFER, vboNormalHandle );
		glBufferData( GL_ARRAY_BUFFER, normals, GL_STATIC_DRAW );
		glNormalPointer( GL_FLOAT, 0, 0L );
		glBindBuffer( GL_ARRAY_BUFFER, 0 );
		return new int[]
		{ vboVertexHandle, vboNormalHandle };
	}

	private static Vector3f parseVertex( String line )
	{
		String[] xyz = line.split( " " );
		float x = Float.valueOf( xyz[ 1 ] );
		float y = Float.valueOf( xyz[ 2 ] );
		float z = Float.valueOf( xyz[ 3 ] );
		return new Vector3f( x, y, z );
	}

	private static Vector3f parseNormal( String line )
	{
		String[] xyz = line.split( " " );
		float x = Float.valueOf( xyz[ 1 ] );
		float y = Float.valueOf( xyz[ 2 ] );
		float z = Float.valueOf( xyz[ 3 ] );
		return new Vector3f( x, y, z );
	}

	private static Model.Face parseFace( boolean hasNormals, String line )
	{
		String[] faceIndices = line.split( " " );
		int[] vertexIndicesArray =
		{ Integer.parseInt( faceIndices[ 1 ].split( "/" )[ 0 ] ), Integer.parseInt( faceIndices[ 2 ].split( "/" )[ 0 ] ), Integer.parseInt( faceIndices[ 3 ].split( "/" )[ 0 ] ) };
		if ( hasNormals )
		{
			int[] normalIndicesArray = new int[ 3 ];
			normalIndicesArray[ 0 ] = Integer.parseInt( faceIndices[ 1 ].split( "/" )[ 2 ] );
			normalIndicesArray[ 1 ] = Integer.parseInt( faceIndices[ 2 ].split( "/" )[ 2 ] );
			normalIndicesArray[ 2 ] = Integer.parseInt( faceIndices[ 3 ].split( "/" )[ 2 ] );
			return new Model.Face( vertexIndicesArray, normalIndicesArray );
		}
		else
		{
			return new Model.Face( ( vertexIndicesArray ) );
		}
	}

	public static Model loadModel( File f ) throws IOException
	{
		BufferedReader reader = new BufferedReader( new FileReader( f ) );
		Model m = new Model();
		String line;
		float smallWidth = 0f;
		float smallHeight = 0f;
		float smallDepth = 0f;
		float bigWidth = 0f;
		float bigHeight = 0f;
		float bigDepth = 0f;
		while ( ( line = reader.readLine() ) != null )
		{
			String prefix = line.split( " " )[ 0 ];
			if ( prefix.equals( "#" ) )
			{
				continue;
			}
			else if ( prefix.equals( "v" ) )
			{				
				Vector3f vertices = parseVertex( line );
				
				m.getVertices().add(vertices );
				
				if( vertices.x < smallWidth )
				{
					smallWidth = vertices.x;
				}
				if( vertices.x > bigWidth )
				{
					bigWidth = vertices.x;
				}
				
				if( vertices.y < smallHeight )
				{
					smallHeight = vertices.y;
				}
				if( vertices.y > bigHeight )
				{
					bigHeight = vertices.y;
				}
				
				if( vertices.z < smallDepth )
				{
					smallDepth = vertices.z;
				}
				if( vertices.z > bigDepth )
				{
					bigDepth = vertices.z;
				}
			}
			else if ( prefix.equals( "vn" ) )
			{
				m.getNormals().add( parseNormal( line ) );
			}
			else if ( prefix.equals( "f" ) )
			{
				m.getFaces().add( parseFace( m.hasNormals(), line ) );
			}
			else
			{
				// throw new
				// RuntimeException("OBJ file contains line which cannot be parsed correctly: "
				// + line);
			}
		}
		reader.close();
		m.setDepth( bigDepth - smallDepth );
		m.setHeight( bigHeight - smallHeight );
		m.setWidth( bigWidth - smallWidth );
		return m;
	}

	public static int createTexturedDisplayList( Model m )
	{		
		int displayList = glGenLists( 1 );
		glNewList( displayList, GL_COMPILE );
		{
			glBegin( GL_TRIANGLES );
			for ( Model.Face face : m.getFaces() )
			{
				if ( face.hasTextureCoordinates() )
				{
					GL11.glBindTexture(GL11.GL_TEXTURE_2D, face.getMaterial().texture.getTextureID() );
					glMaterial( GL_FRONT, GL_DIFFUSE, BufferTools.asFlippedFloatBuffer( face.getMaterial().diffuseColour[ 0 ], face.getMaterial().diffuseColour[ 1 ], face.getMaterial().diffuseColour[ 2 ], 1 ) );
					glMaterial( GL_FRONT, GL_AMBIENT, BufferTools.asFlippedFloatBuffer( face.getMaterial().ambientColour[ 0 ], face.getMaterial().ambientColour[ 1 ], face.getMaterial().ambientColour[ 2 ], 1 ) );
					glMaterialf( GL_FRONT, GL_SHININESS, face.getMaterial().specularCoefficient );
				}
				if ( face.hasNormals() )
				{
					Vector3f n1 = m.getNormals().get( face.getNormalIndices()[ 0 ] - 1 );
					glNormal3f( n1.x, n1.y, n1.z );
				}
				if ( face.hasTextureCoordinates() )
				{
					Vector2f t1 = m.getTextureCoordinates().get( face.getTextureCoordinateIndices()[ 0 ] - 1 );
					glTexCoord2f( t1.x, t1.y );
				}
				Vector3f v1 = m.getVertices().get( face.getVertexIndices()[ 0 ] - 1 );
				glVertex3f( v1.x, v1.y, v1.z );
				if ( face.hasNormals() )
				{
					Vector3f n2 = m.getNormals().get( face.getNormalIndices()[ 1 ] - 1 );
					glNormal3f( n2.x, n2.y, n2.z );
				}
				if ( face.hasTextureCoordinates() )
				{
					Vector2f t2 = m.getTextureCoordinates().get( face.getTextureCoordinateIndices()[ 1 ] - 1 );
					glTexCoord2f( t2.x, t2.y );
				}
				Vector3f v2 = m.getVertices().get( face.getVertexIndices()[ 1 ] - 1 );
				glVertex3f( v2.x, v2.y, v2.z );
				if ( face.hasNormals() )
				{
					Vector3f n3 = m.getNormals().get( face.getNormalIndices()[ 2 ] - 1 );
					glNormal3f( n3.x, n3.y, n3.z );
				}
				if ( face.hasTextureCoordinates() )
				{
					Vector2f t3 = m.getTextureCoordinates().get( face.getTextureCoordinateIndices()[ 2 ] - 1 );
					glTexCoord2f( t3.x, t3.y );
				}
				Vector3f v3 = m.getVertices().get( face.getVertexIndices()[ 2 ] - 1 );
				glVertex3f( v3.x, v3.y, v3.z );
			}
			glEnd();
		}
		glEndList();
		return displayList;
	}

	public static Model loadTexturedModel( File f ) throws IOException
	{
		BufferedReader reader = new BufferedReader( new FileReader( f ) );
		Model m = new Model();
		Model.Material currentMaterial = new Model.Material();
		String line;
		float smallWidth = 0f;
		float smallHeight = 0f;
		float smallDepth = 0f;
		float bigWidth = 0f;
		float bigHeight = 0f;
		float bigDepth = 0f;
		while ( ( line = reader.readLine() ) != null )
		{
			if ( line.startsWith( "#" ) || line.equals(  "" ) || line.equals( " " ) )
			{
				continue;
			}
			if ( line.startsWith( "mtllib " ) )
			{
				String materialFileName = line.split( " " )[ 1 ];
				File materialFile = new File( "res/materials/" + materialFileName );
				BufferedReader materialFileReader = new BufferedReader( new FileReader( materialFile ) );
				String materialLine;
				Model.Material parseMaterial = new Model.Material();
				String parseMaterialName = "";
				while ( ( materialLine = materialFileReader.readLine() ) != null )
				{
					if ( materialLine.startsWith( "#" ) || materialLine.equals(  "" ) || materialLine.equals( " " ) )
					{
						continue;
					}
					if ( materialLine.startsWith( "newmtl " ) )
					{
						if ( ! parseMaterialName.equals( "" ) )
						{
							m.getMaterials().put( parseMaterialName, parseMaterial );
						}
						parseMaterialName = materialLine.split( " " )[ 1 ];
						parseMaterial = new Model.Material();
					}
					else if ( materialLine.startsWith( "Ns " ) )
					{
						parseMaterial.specularCoefficient = Float.valueOf( materialLine.split( " " )[ 1 ] );
					}
					else if ( materialLine.startsWith( "Ka " ) )
					{
						String[] rgb = materialLine.split( " " );
						parseMaterial.ambientColour[ 0 ] = Float.valueOf( rgb[ 1 ] );
						parseMaterial.ambientColour[ 1 ] = Float.valueOf( rgb[ 2 ] );
						parseMaterial.ambientColour[ 2 ] = Float.valueOf( rgb[ 3 ] );
					}
					else if ( materialLine.startsWith( "Ks " ) )
					{
						String[] rgb = materialLine.split( " " );
						parseMaterial.specularColour[ 0 ] = Float.valueOf( rgb[ 1 ] );
						parseMaterial.specularColour[ 1 ] = Float.valueOf( rgb[ 2 ] );
						parseMaterial.specularColour[ 2 ] = Float.valueOf( rgb[ 3 ] );
					}
					else if ( materialLine.startsWith( "Kd " ) )
					{
						String[] rgb = materialLine.split( " " );
						parseMaterial.diffuseColour[ 0 ] = Float.valueOf( rgb[ 1 ] );
						parseMaterial.diffuseColour[ 1 ] = Float.valueOf( rgb[ 2 ] );
						parseMaterial.diffuseColour[ 2 ] = Float.valueOf( rgb[ 3 ] );
					}
					else if ( materialLine.startsWith( "map_Kd" ) )
					{
						System.out.println("loading: res/textures/" + f.getName().replace( ".obj", "/" ) + materialLine.split( " " )[ 1 ]);
						parseMaterial.texture = TextureLoader.getTexture( "PNG", new FileInputStream( new File( "res/textures/" + f.getName().replace( ".obj", "/" ) + materialLine.split( " " )[ 1 ] ) ) );
					}
					else
					{
						System.err.println( "[MTL] Unknown Line: " + materialLine );
					}
				}
				m.getMaterials().put( parseMaterialName, parseMaterial );
				materialFileReader.close();
			}
			else if ( line.startsWith( "usemtl " ) )
			{
				currentMaterial = m.getMaterials().get( line.split( " " )[ 1 ] );
			}
			else if ( line.startsWith( "v " ) )
			{				
				Vector3f vertices = parseVertex( line );
				
				m.getVertices().add(vertices );
				
				if( vertices.x < smallWidth )
				{
					smallWidth = vertices.x;
				}
				if( vertices.x > bigWidth )
				{
					bigWidth = vertices.x;
				}
				
				if( vertices.y < smallHeight )
				{
					smallHeight = vertices.y;
				}
				if( vertices.y > bigHeight )
				{
					bigHeight = vertices.y;
				}
				
				if( vertices.z < smallDepth )
				{
					smallDepth = vertices.z;
				}
				if( vertices.z > bigDepth )
				{
					bigDepth = vertices.z;
				}
			}
			else if ( line.startsWith( "vn " ) )
			{
				String[] xyz = line.split( " " );
				float x = Float.valueOf( xyz[ 1 ] );
				float y = Float.valueOf( xyz[ 2 ] );
				float z = Float.valueOf( xyz[ 3 ] );
				m.getNormals().add( new Vector3f( x, y, z ) );
			}
			else if ( line.startsWith( "vt " ) )
			{
				String[] xyz = line.split( " " );
				float s = Float.valueOf( xyz[ 1 ] );
				float t = Float.valueOf( xyz[ 2 ] );
				m.getTextureCoordinates().add( new Vector2f( s, t ) );
			}
			else if ( line.startsWith( "f " ) )
			{
				String[] faceIndices = line.split( " " );
				int[] vertexIndicesArray =
				{ Integer.parseInt( faceIndices[ 1 ].split( "/" )[ 0 ] ), Integer.parseInt( faceIndices[ 2 ].split( "/" )[ 0 ] ), Integer.parseInt( faceIndices[ 3 ].split( "/" )[ 0 ] ) };
				int[] textureCoordinateIndicesArray =
				{ - 1, - 1, - 1 };
				if ( m.hasTextureCoordinates() )
				{
					textureCoordinateIndicesArray[ 0 ] = Integer.parseInt( faceIndices[ 1 ].split( "/" )[ 1 ] );
					textureCoordinateIndicesArray[ 1 ] = Integer.parseInt( faceIndices[ 2 ].split( "/" )[ 1 ] );
					textureCoordinateIndicesArray[ 2 ] = Integer.parseInt( faceIndices[ 3 ].split( "/" )[ 1 ] );
				}
				int[] normalIndicesArray =
				{ 0, 0, 0 };
				if ( m.hasNormals() )
				{
					normalIndicesArray[ 0 ] = Integer.parseInt( faceIndices[ 1 ].split( "/" )[ 2 ] );
					normalIndicesArray[ 1 ] = Integer.parseInt( faceIndices[ 2 ].split( "/" )[ 2 ] );
					normalIndicesArray[ 2 ] = Integer.parseInt( faceIndices[ 3 ].split( "/" )[ 2 ] );
				}
			
				m.getFaces().add( new Model.Face( vertexIndicesArray, normalIndicesArray, textureCoordinateIndicesArray, currentMaterial ) );
			}
			else if ( line.startsWith( "s " ) )
			{
				boolean enableSmoothShading = ! line.contains( "off" );
				m.setSmoothShadingEnabled( enableSmoothShading );
			}
			else
			{
				System.err.println( "[OBJ] Unknown Line: " + line );
			}
		}
		reader.close();
		m.setDepth( bigDepth - smallDepth );
		m.setHeight( bigHeight - smallHeight );
		m.setWidth( bigWidth - smallWidth );
		return m;
	}
}